#include "Object.h"
#include "Punto.h"
#include "Linea.h"
#include "Triangulo.h"
#include "oglArrays.h"
// #include "pmfonts.h"
#include "ply_import.h"
#include "gid_export.h"
// #include "file_browser.h"
// #include "AppFrameWork.h"

static void calculate_normals( Vector *lst_normals, const Punto *lst_puntos, const Triangulo *lst_triangs, 
                               int num_puntos, int num_triangles) {
  for ( int in = 0; in < num_puntos; in++) {
    lst_normals[ in].set( 0.0f, 0.0f, 0.0f);
  }
  for ( int it = 0; it < num_triangles; it++) {
    Punto normal_tri = lst_triangs[ it].get_normal( lst_puntos);
    lst_normals[ lst_triangs[ it].v1()] += normal_tri;
    lst_normals[ lst_triangs[ it].v2()] += normal_tri;
    lst_normals[ lst_triangs[ it].v3()] += normal_tri;
  }
  for ( int in = 0; in < num_puntos; in++) {
    lst_normals[ in].normalize();
  }
}

static void swap_normals_sense( Vector *lst_normals, int num_normals) {
  for ( int in = 0; in < num_normals; in++) {
    lst_normals[ in] = -lst_normals[ in];
  }
}

void Object::SwapNormals() {
  swap_normals_sense( _lst_normales, _num_puntos);
  set_dirty( true); // recreate VBO / VA
}

static void swap_orientacion_triangulos( Triangulo *lst_triangs, int num_triangs) {
  for ( int it = 0; it < num_triangs; it++) {
    lst_triangs[ it].swap_sense();
  }
}

void Object::SwapTrianglesSense() {
  swap_orientacion_triangulos( _lst_triangulos, _num_triangulos);
  // recalculate normals...
  calculate_normals( _lst_normales, _lst_puntos, _lst_triangulos, _num_puntos, _num_triangulos);
  set_dirty( true); // recreate VBO / VA
}

void Object::ActualizeBBox() {
  if ( !_lst_puntos || !_num_puntos) return;
  _caja_contenedora.init( _lst_puntos[ 0]);
  for( int ip = 0; ip < _num_puntos; ip++) {
    _caja_contenedora.update( _lst_puntos[ ip]);
  }
}

void Object::SetData( int num_points, int num_lines, int num_triangles, int num_quads, 
                      const float *lst_points, 
                      const int *lst_lines, const int *lst_triangles, const int *lst_quads, 
                      const float *lst_normals, const float *lst_scalar) {
  reset();

  // primero puntos
  _lst_puntos = new Punto[ num_points];
  
  _caja_contenedora.init( lst_points[ 0], lst_points[ 1], lst_points[ 2]);
  
  for( int ip = 0, idx = 0; ip < num_points; ip++, idx += 3) {
    _lst_puntos[ ip].set( lst_points[ idx], lst_points[ idx + 1], lst_points[ idx + 2]);
    _caja_contenedora.update( lst_points[ idx], lst_points[ idx + 1], lst_points[ idx + 2]);
  }
  _num_puntos = num_points;
  
  // lineas:
  _lst_lineas = new Linea[ num_lines];
  for ( int il = 0, idx = 0; il < num_lines; il++, idx += 2) {
    _lst_lineas[ il].set( lst_lines[ idx], lst_lines[ idx + 1], il);
  }
  _num_lineas = num_lines;

  // caras:
  // at the moment we transform quads into triangles 
  _lst_triangulos = new Triangulo[ num_triangles + 2 * num_quads];
  int it = 0;
  for ( int idx = 0; it < num_triangles; it++, idx += 3) {
    _lst_triangulos[ it].set( lst_triangles[ idx], lst_triangles[ idx + 1], lst_triangles[ idx + 2], it);
  }
  for ( int ic = 0, idx = 0; ic < num_quads; ic++, it += 2, idx += 4) {
    _lst_triangulos[ it].set( lst_quads[ idx], lst_quads[ idx + 1], lst_quads[ idx + 2], it);
    _lst_triangulos[ it + 1].set( lst_quads[ idx + 2], lst_quads[ idx + 3], lst_quads[ idx], it + 1);
  }
  _num_triangulos = num_triangles + 2 * num_quads;

  _lst_normales = new Vector[ num_points];
  // if ( lst_normals) {
  //   for( int ip = 0, idx = 0; ip < num_points; ip++, idx += 3) {
  //     _lst_normales[ ip].set( lst_normals[ idx], lst_normals[ idx + 1], lst_normals[ idx + 2]);
  //   }
  // } else {
  //   // calculate normals...
  calculate_normals( _lst_normales, _lst_puntos, _lst_triangulos, _num_puntos, _num_triangulos);
  // }

  if ( lst_scalar) {
    _lst_scalars = new Scalars *[ 2];
    _lst_scalars[ 0] = new Scalars( "Scalar", num_points);
    _lst_scalars[ 0]->set_scalars( lst_scalar);
    _lst_scalars[ 1] = NULL;
    //, num_points * sizeof( float));
  }
}

void Object::SetData( int num_points, int num_lines, int num_triangles, 
                      const Punto *lst_points, const Linea *lst_lines, const Triangulo *lst_triangles) {
  reset();

  // primero puntos
  _lst_puntos = new Punto[ num_points];
  
  _caja_contenedora.init( lst_points[ 0]);
  for( int ip = 0; ip < num_points; ip++) {
    _lst_puntos[ ip] = lst_points[ ip];
    _caja_contenedora.update( lst_points[ ip]);
  }
  _num_puntos = num_points;

  // lineas:
  _lst_lineas = new Linea[ num_lines];
  for ( int il = 0; il < num_lines; il++) {
    _lst_lineas[ il] = lst_lines[ il];
  }
  _num_lineas = num_lines;

  // caras:
  _lst_triangulos = new Triangulo[ num_triangles];
  for ( int it = 0; it < num_triangles; it++) {
    _lst_triangulos[ it] = lst_triangles[ it];
  }
  _num_triangulos = num_triangles;
  
  _lst_normales = new Vector[ num_points];
  calculate_normals( _lst_normales, _lst_puntos, _lst_triangulos, _num_puntos, _num_triangulos);
}

void Object::reset() {
  set_dirty( true);
  set_name( NULL);
  if ( _lst_puntos) delete[] _lst_puntos;
  _lst_puntos = NULL;
  _num_puntos = 0;
  if ( _lst_lineas) delete[] _lst_lineas;
  _lst_lineas = NULL;
  _num_lineas = 0;
  if ( _lst_triangulos) delete[] _lst_triangulos;
  _lst_triangulos = NULL;
  _num_triangulos = 0;
  if ( _lst_normales) delete[] _lst_normales;
  _lst_normales = NULL;
  if ( _lst_scalars) {
    for ( int idx = 0; _lst_scalars[ idx]; idx++) {
      delete _lst_scalars[ idx];
    }
    delete[] _lst_scalars;
  }
  _lst_scalars = NULL;
  _accum_rot_y = 0.0f;
}

float Object::diagonal() const {
  float diag = ( float)2.0;//1.4422495703074083823216383107801;
  if ( _lst_puntos) {
    diag = _caja_contenedora.diagonal();
  }
  return diag;
}

Punto Object::centro() const {
  if ( !_lst_puntos)
    return Punto( 0.0f, 0.0f, 0.0f);
  else
    return _caja_contenedora.centro();
}

void Object::ResetOpenGLBO() {
  if ( _ogl_array) delete _ogl_array;
  _ogl_array = NULL;
}

void Object::SetRenderMethod( const _t_ogl_render_method rm) {
  set_dirty( true);
  OglArray tmp;
  // check if it's possible
  // for instance: vbo is not available on opengl < 1.5
  tmp.setRenderMethod( rm);
  _render_method = tmp.getRenderMethod();
}

int Object::CreateOpenGLBO() {
  if ( _dirty) {
    _dirty = false;
    ResetOpenGLBO();
  }
  
  if ( _ogl_array) return 1;
  if ( !_num_puntos) return 0;
  
  _ogl_array = new OglArray();

  // _ogl_array->setUseVBO( GL_TRUE); // if GL_FALSE (default) uses Vertex Arrays
  // _ogl_array->setRenderMethod( _render_method);
  _ogl_array->setRenderMethod( OGLARRAY_VERTEX_ARRAY);//_render_method);
  
  _t_ogl_vector *lst_pnts = ( _t_ogl_vector *)malloc( sizeof( _t_ogl_vector) * _num_puntos);
  _t_ogl_vector *lst_nrms = ( _t_ogl_vector *)malloc( sizeof( _t_ogl_vector) * _num_puntos);
  for ( int ip = 0; ip < _num_puntos; ip++) {
    lst_pnts[ ip].x = _lst_puntos[ ip].x();
    lst_pnts[ ip].y = _lst_puntos[ ip].y();
    lst_pnts[ ip].z = _lst_puntos[ ip].z();
    lst_nrms[ ip].x = _lst_normales[ ip].x();
    lst_nrms[ ip].y = _lst_normales[ ip].y();
    lst_nrms[ ip].z = _lst_normales[ ip].z();
  }
  _ogl_array->setVertexLists( lst_pnts, lst_nrms, _num_puntos, NULL, 0);
  free( lst_pnts);
  free( lst_nrms);

  GLuint *lst_lin = ( GLuint *)malloc( sizeof( GLuint) * 2 * _num_lineas);
  for ( int il = 0, idx = 0; il < _num_lineas; il++, idx += 2) {
    lst_lin[ idx] = ( GLuint)_lst_lineas[ il].v1();
    lst_lin[ idx + 1] = ( GLuint)_lst_lineas[ il].v2();
  }
  _ogl_array->setLineList( lst_lin, _num_lineas);
  free( lst_lin);

  GLuint *lst_tri = ( GLuint *)malloc( sizeof( GLuint) * 3 * _num_triangulos);
  // para Intel:
  int triangs2draw = _num_triangulos;
  // if ( _num_triangulos > 8000000)
  //   triangs2draw = 8000000;
  for ( int it = 0, idx = 0; it < triangs2draw; it++, idx += 3) {
    lst_tri[ idx] = ( GLuint)_lst_triangulos[ it].v1();
    lst_tri[ idx + 1] = ( GLuint)_lst_triangulos[ it].v2();
    lst_tri[ idx + 2] = ( GLuint)_lst_triangulos[ it].v3();
  }
  _ogl_array->setTriangleList( lst_tri, triangs2draw);
  free( lst_tri);

  // if ( _lst_scalar) {
  //   _ogl_array->setVertexColors( _lst_scalar, 1);
  // }
  
  return 1;
}

int Object::RenderOpenGLBO( _t_obj_draw_mode draw_mode) const {
  switch( draw_mode) {
    case OBJ_DRAW_EDGES:
      glPolygonMode( GL_FRONT_AND_BACK, GL_LINE);
      _ogl_array->renderVALists( _draw_which_elements);
      glPolygonMode( GL_FRONT_AND_BACK, GL_FILL);
      break;
    case OBJ_DRAW_POLYGONS:
      glLineWidth( 3.0f);
      _ogl_array->renderVALists( _draw_which_elements);
      glLineWidth( 1.0f);
      break;
    case OBJ_DRAW_EDGES_POLYGONS:
      glPushAttrib( GL_LIGHTING_BIT | GL_POLYGON_BIT | GL_CURRENT_BIT | GL_ENABLE_BIT);
      
      glEnable( GL_COLOR_MATERIAL);
      glColorMaterial( GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE);
      
      glColor4f( 0.0f, 0.0f, 0.0f, 1.0f);
      glPolygonMode( GL_FRONT_AND_BACK, GL_LINE);
      _ogl_array->renderVALists( _draw_which_elements);
      glPolygonMode( GL_FRONT_AND_BACK, GL_FILL);
      
      glPopAttrib();
      glEnable( GL_POLYGON_OFFSET_FILL);
      glPolygonOffset( 1.0, 1.0);
      glLineWidth( 3.0f);
      _ogl_array->renderVALists( _draw_which_elements);
      glLineWidth( 1.0f);
      glDisable( GL_POLYGON_OFFSET_FILL);
      break;
  }
  return 1;
}

// void Object::drawTeapot( _t_obj_draw_mode draw_mode) const {
//   switch( draw_mode) {
//     case OBJ_DRAW_EDGES:
//       glPolygonMode( GL_FRONT_AND_BACK, GL_LINE);
//       glutWireTeapot( 1.0f);
//       glPolygonMode( GL_FRONT_AND_BACK, GL_FILL);
//       break;
//     case OBJ_DRAW_POLYGONS:
//       glutSolidTeapot( 1.0f);
//       break;
//     case OBJ_DRAW_EDGES_POLYGONS:
//       glPushAttrib( GL_LIGHTING_BIT | GL_POLYGON_BIT | GL_CURRENT_BIT | GL_ENABLE_BIT);
//       
//       glEnable( GL_COLOR_MATERIAL);
//       glColorMaterial( GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE);
//       
//       glColor4f( 0.0f, 0.0f, 0.0f, 1.0f);
//       glPolygonMode( GL_FRONT_AND_BACK, GL_LINE);
//       glutWireTeapot( 1.0f);
//       glPolygonMode( GL_FRONT_AND_BACK, GL_FILL);
//       
//       glPopAttrib();
//       glEnable( GL_POLYGON_OFFSET_FILL);
//       glPolygonOffset( 1.0, 1.0);
//       glutSolidTeapot( 1.0f);
//       glDisable( GL_POLYGON_OFFSET_FILL);
//       break;
//   }
//   return;
// }

// void Object::drawSphere( _t_obj_draw_mode draw_mode) const {
//   switch( draw_mode) {
//     case OBJ_DRAW_EDGES:
//       glPolygonMode( GL_FRONT_AND_BACK, GL_LINE);
//       glutWireSphere( 10.0f, 10, 10);
//       glPolygonMode( GL_FRONT_AND_BACK, GL_FILL);
//       break;
//     case OBJ_DRAW_POLYGONS:
//       glutSolidSphere( 10.0f, 20, 10);
//       break;
//     case OBJ_DRAW_EDGES_POLYGONS:
//       glPushAttrib( GL_LIGHTING_BIT | GL_POLYGON_BIT | GL_CURRENT_BIT | GL_ENABLE_BIT);
//       
//       glEnable( GL_COLOR_MATERIAL);
//       glColorMaterial( GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE);
//       
//       glColor4f( 0.0f, 0.0f, 0.0f, 1.0f);
//       glPolygonMode( GL_FRONT_AND_BACK, GL_LINE);
//       glutWireSphere( 10.0f, 20, 10);
//       glPolygonMode( GL_FRONT_AND_BACK, GL_FILL);
//       
//       glPopAttrib();
//       glEnable( GL_POLYGON_OFFSET_FILL);
//       glPolygonOffset( 1.0, 1.0);
//       glutSolidSphere( 10.0f, 20, 10);
//       glDisable( GL_POLYGON_OFFSET_FILL);
//       break;
//   }
//   return;
// }

void Object::draw() {
  CheckGLError( "Object::draw begin");

  // if ( !_lst_puntos) {
  // }
  
  if ( !_lst_puntos) {
    // // drawTeapot( _current_view_mode);
    // cull_on();
    // drawSphere( _current_view_mode);
    // cull_off();
    // CheckGLError( "Object::draw end teapot");
    return;
  }

  // // shader example
  // static OglProgram *sh_prog = NULL;
  // static bool sh_prog_ok = false;
  // if ( !sh_prog) {
  //   // sh_prog = new OglProgram( "simple.vert", NULL, "red.frag");
  //   sh_prog = new OglProgram( "simple.vert", NULL, "depth_as_color.frag");
  //   sh_prog_ok = sh_prog->create_program();
  //   if ( !sh_prog_ok) {
  //     // printf( "Could not compile sh program\n");
  //     WarnWinFixedWidth( sh_prog->get_error());
  //     PrintOutput( sh_prog->get_error());
  //   }
  // }
  // if ( sh_prog_ok && sh_prog) {
  //   sh_prog->begin();
  // }

  // Punto centro = _caja_contenedora.centro();
  // glTranslatef( -centro.x(), -centro.y(), -centro.z());

  CreateOpenGLBO();

  Punto cent = this->centro();
  if ( _accum_rot_y != 0.0f) {
    glPushMatrix();
    Punto cent = this->centro();
    glTranslatef( cent.x(), cent.y(), cent.z());
    glRotatef( _accum_rot_y, 0.0f, 1.0f, 0.0f);
    glTranslatef( -cent.x(), -cent.y(), -cent.z());
  }
  
  cull_on();
  RenderOpenGLBO( _current_view_mode);
  cull_off();
  
  // draw bounding box
  if ( _draw_bbox) {
    glPushAttrib( GL_LIGHTING_BIT | GL_CURRENT_BIT | GL_ENABLE_BIT); // to save current ( object) color too
    glEnable( GL_COLOR_MATERIAL);
    glColorMaterial( GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE);
    glDisable( GL_LIGHTING);
    glColor4f( 1.0f, 1.0f, 1.0f, 1.0f);
    _caja_contenedora.draw();
    glPopAttrib();
  }

  if ( _accum_rot_y != 0.0f) {
    glPopMatrix();
}
  
  // // undo the translation
  // glTranslatef( centro.x(), centro.y(), centro.z());
  
  // if ( sh_prog_ok && sh_prog) {
  //   sh_prog->end();
  // }
  CheckGLError( "Object::draw end vbo");
}

int Object::WriteGidFiles( const char *filename) const {
  return write_gid_files( filename,
                          this->get_name(),
                          this->get_num_puntos(), this->get_lst_puntos(), 
                          this->get_num_triangulos(), this->get_lst_triangulos(),
                          0, NULL, // at the moment the object has no lines
                          this->get_lst_scalars());
}

int Object::WritePlyFile( const char *filename) const {
  return write_ply_file( filename,
                         this->get_name(),
                         this->get_num_puntos(), this->get_lst_puntos(), 
                         this->get_num_triangulos(), this->get_lst_triangulos(), 
                         this->get_lst_scalars());
}

void Object::scale_to( float max_dim) {
  // ActualizeBBox();
  Box cubo_contenedor = _caja_contenedora.get_centered_bounding_cube();
  Punto centre = cubo_contenedor.centro();
  Punto diff = ( cubo_contenedor.max() - cubo_contenedor.min());
  // Punto inv_dif( 1.0f / diff.x(), 1.0f / diff.y(), 1.0f / diff.z());
  Punto inv_dif( max_dim / diff.x(), max_dim / diff.y(), max_dim / diff.z());
  Punto *tmp_lst_puntos = _lst_puntos;
  _lst_puntos = new Punto[ _num_puntos];
  for ( int ip = 0; ip < _num_puntos; ip++) {
    _lst_puntos[ ip] = component_product( ( tmp_lst_puntos[ ip] - centre), inv_dif);
  }
  delete[] tmp_lst_puntos;
  ActualizeBBox();
  set_dirty( true);
}

void Object::set_centre( const Punto &new_cent) {
  // ActualizeBBox();
  Punto cent = this->centro();
  Punto dif = new_cent - cent;
  for ( int ip = 0; ip < _num_puntos; ip++) {
    _lst_puntos[ ip] += dif;
  }
  ActualizeBBox();
  set_dirty( true);
}

void Object::fit_into( const Box &space) {
  // ActualizeBBox();
  // height = y axis
  
  // rotate? only between x and z
  Punto space_dim = space.diff();
  int bigest_space_axis = space_dim.x() >= space_dim.z() ? 0 : 2;
  Punto object_dim = _caja_contenedora.diff();
  int bigest_object_axis = object_dim.x() >= object_dim.z() ? 0 : 2;
  if ( bigest_space_axis != bigest_object_axis) {
    // swap x <--> z
    for ( int ip = 0; ip < _num_puntos; ip++) {
      float tmp = _lst_puntos[ ip].x();
      _lst_puntos[ ip].x() = _lst_puntos[ ip].z();
      _lst_puntos[ ip].z() = tmp;
    }
    ActualizeBBox();
    object_dim = _caja_contenedora.diff();
  }

  // float max_dim = space_dim[ bigest_space_axis] > space_dim[ 1] ?
  //     space_dim[ bigest_space_axis] : space_dim[ 1];
  // scale_to( max_dim);
  float min_dim = space_dim[ bigest_space_axis] < space_dim[ 1] ?
      space_dim[ bigest_space_axis] : space_dim[ 1];
  scale_to( min_dim);

  set_centre( space.centro());
  
  ActualizeBBox();
  set_dirty( true);
}

void Object::rotate_y( float degrees) {
  _accum_rot_y += degrees;  
  // ActualizeBBox();
  // set_dirty( true);
}
